﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Threading.Tasks;
using System.Transactions;
using Codeflows.Extensions.Fx.System.Reflection;
using Codeflows.Extensions.Tracking;
using Codeflows.Logging;
using Codeflows.Tracking;
using Codeflows.Results;
using Codeflows.Dispatch;

namespace Codeflows
{
    public static class CodeflowApplication
    {
        private static readonly TransactionOptions DefaultTransactionOptions = new TransactionOptions { IsolationLevel = IsolationLevel.ReadCommitted };
		private static ICodeflowTrackerFactory _trackerFactory = new DefaultCodeflowTrackerFactory();
		private static ICodeflowFactory _codeflowFactory = new DefaultCodeflowFactory();

		public static void SetCodeflowTrackerFactory(ICodeflowTrackerFactory instance)
		{
			_trackerFactory = instance;
		}

		public static void SetCodeflowFactory(ICodeflowFactory instance)
		{
			_codeflowFactory = instance;
		}

        public static void SetLogProvider(ILogProvider logProvider)
        {
            DiagnosticsUtility.SetLogProvider(logProvider);
        }

        public static Guid Create<T>(Expression<Func<T, Result>> startAction) 
			where T : Codeflow
        {
            if (startAction == null) throw new ArgumentNullException("startAction");
            var parsedExpression = new ArgumentParser<T>().Parse(startAction);
			return CreateCore(typeof(T), parsedExpression);
        }

        public static Guid Create<T>(Expression<Func<T, Task<Result>>> startAction) 
			where T : Codeflow
        {
            if (startAction == null) throw new ArgumentNullException("startAction");
            var parsedExpression = new ArgumentParser<T>().Parse(startAction);
            return CreateCore(typeof (T), parsedExpression);
        }
		
        public static Result Run<T>(this T codeflow, Guid codeflowId, TimeSpan timeout = default(TimeSpan)) 
			where T : Codeflow
        {
            if (codeflow == null) throw new ArgumentNullException("codeflow");
            var codeflowInstance = _trackerFactory.ReadRunUpdate(codeflowId, c => c.Own(TimeoutOrDefault(timeout)));                        
            return new Dispatcher(codeflowInstance.DispatcherId, codeflowInstance, codeflow, _trackerFactory,
                                  ActionMethodBuilder.Build(codeflow)).Execute();            
        }

        public static Result Run(Guid codeflowId, TimeSpan timeout = default(TimeSpan))
        {
            var codeflowInstance = _trackerFactory.ReadRunUpdate(codeflowId, c => c.Own(TimeoutOrDefault(timeout)));
            using(var instance = _codeflowFactory.CreateCodeflow(Type.GetType(codeflowInstance.CodeflowType, true)))
            {
                return
                    new Dispatcher(codeflowInstance.DispatcherId, codeflowInstance, instance.Instance, _trackerFactory,
                                   ActionMethodBuilder.Build(instance.Instance)).Execute();
            }            
        }

        public static Result Resume<T>(this T codeflow, Guid codeflowId, Expression<Func<T, Result>> action, TimeSpan timeout = default(TimeSpan))
            where T : Codeflow
        {
            if (codeflow == null) throw new ArgumentNullException("codeflow");
            if (action == null) throw new ArgumentNullException("action");

            var parsedExpression = new ArgumentParser<T>().Parse(action);
            return ResumeCore(codeflow, codeflowId, parsedExpression, timeout);
        }

		public static Result Resume<T>(this T codeflow, Guid codeflowId, Expression<Func<T, Task<Result>>> action, TimeSpan timeout = default(TimeSpan))
            where T : Codeflow
        {
            if (action == null) throw new ArgumentNullException("action");

            var parsedExpression = new ArgumentParser<T>().Parse(action);
            return ResumeCore(codeflow, codeflowId, parsedExpression, timeout);
        }

		public static int TryRunNext(TimeSpan timeout = default(TimeSpan), int count = 1)
		{
            var codeflowInstances = _trackerFactory.ReadNextRunUpdate(c => c.Own(TimeoutOrDefault(timeout)), count).ToArray();

		    Dispatch(codeflowInstances);

            return codeflowInstances.Length;
        }

		public static int RunFromReadyList(TimeSpan timeout = default(TimeSpan), int count = 1)
        {
            var codeflowInstances = _trackerFactory.ForEachReadyInstance(c => c.Own(TimeoutOrDefault(timeout)), count).ToArray();

		    Dispatch(codeflowInstances);

		    return codeflowInstances.Length;
        }

        static void Dispatch(IEnumerable<CodeflowInstance> codeflowInstances)
        {
            foreach (var codeflowInstance in codeflowInstances)
            {
                var instance = codeflowInstance;
                Task.Factory.StartNew(() => DispatchCore(instance));
            }           
        }

        static void DispatchCore(CodeflowInstance instance)
        {
            var codeflowType = Type.GetType(instance.CodeflowType);
            var codeflowContext = _codeflowFactory.CreateCodeflow(codeflowType);
            var codeflow = codeflowContext.Instance;

            try
            {
                var result = new Dispatcher(instance.DispatcherId,
                                        instance,
                                        codeflow,
                                        _trackerFactory,
                                        ActionMethodBuilder.Build(codeflow)).Execute();

                var pending = result as PendingResult;
                if (pending == null)
                {
                    codeflowContext.Dispose();
                    return;
                }

                var completion = new AsyncCompletion(codeflowContext);
                pending.PendingTask.ContinueWith(completion.Complete);
            }
            catch (CodeflowException ex)
            {
               DiagnosticsUtility.Error("Unhandled exception executing codeflow.", ex);
            }            
        }

        static Guid CreateCore(Type type, ParsedExpression parsedExpression)
        {
            using (var scope = new TransactionScope(TransactionScopeOption.RequiresNew, DefaultTransactionOptions))
            {
                Guid codeflowInstanceId;
                var codeflowInstance = new CodeflowInstance(type.AssemblyQualifiedName,
                                                                type.GetFriendlyName(),
                                                                parsedExpression.ActionName, parsedExpression.Arguments);
                using (var tracker = _trackerFactory.CreateTracker())
                {                   
                    codeflowInstanceId = tracker.Instance.Create(codeflowInstance);
                }
                scope.Complete();
                return codeflowInstanceId;
            }            
        }

        static Result ResumeCore<T>(T codeflow, Guid codeflowId, ParsedExpression parsedExpression, TimeSpan timeout)
            where T : Codeflow
        {
            var codeflowInstance = _trackerFactory.ReadRunUpdate(codeflowId, c => c.Own(TimeoutOrDefault(timeout)));

            return new Dispatcher(codeflowInstance.DispatcherId, codeflowInstance, codeflow, _trackerFactory,
                                  ActionMethodBuilder.Build(codeflow)).ExecuteFrom(parsedExpression.ActionName,
                                                                                   parsedExpression.Arguments);            
        }

        static TimeSpan TimeoutOrDefault(TimeSpan timeout)
        {
            return timeout == TimeSpan.Zero ? TimeSpan.FromMinutes(5) : timeout;
        }       
    }
}